/*
 * pixiv_down - CLI-based downloading tool for https://www.pixiv.net.
 * Copyright (C) 2023, 2024  Mio
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
module app.cmds.compact;

import std.experimental.logger;

import pd.configuration: Config;
import pd.pixiv;

public void displayCompactHelp()
{
   import std.stdio : stderr;

   stderr.writefln(
      "pixiv_down compact - Compact an account's directories in to one\n" ~
      "\nUsage:\tpixiv_down compact [options]\n" ~
      "\nThis command will determine all pixiv account's that have\n" ~
      "multiple directories, and compact them in to one.  By default\n" ~
      "the current account display name is used.  You can use the\n" ~
      "\"--interactive\" option to select a name to use.\n" ~
      "\nOptions:\n" ~
      "   -h, --help       \tDisplay this help message and exit.\n" ~
      "   -i, --interactive\tSelect the name for each account.\n" ~
      "   -n, --dry-run    \tPrint the directories that would be moved,\n" ~
      "                    \tbut do not actually move them.\n" ~
      "\nNOTE: Multiple directories can occur as people change their display\n" ~
      "name on pixiv.");
}

public int compactHandle(string[] args, const ref Config config)
{
   import std.getopt : getopt, GetOptException, GetOptOption = config;
   import std.stdio : stderr;

   Options options;

   try {
      auto helpInformation = getopt(args,
         GetOptOption.bundling,
         "interactive|i", &options.interactive,
         "dry-run|n", &options.dryRun);

      if (helpInformation.helpWanted) {
         displayCompactHelp();
         return 0;
      }
   } catch (GetOptException e) {
      stderr.writefln("pixiv_down compact: %s", e.msg);
      stderr.writefln("Run 'pixiv_down help compact' for more information");
      return 1;
   }

   compactAccounts(config, options);
   return 0;
}

private:

import std.typecons : Tuple;

alias PairType = Tuple!(string, "key", string[], "value");

struct Options
{
   bool interactive = false;
   bool dryRun = false;
}

void compactAccounts(const ref Config config, const ref Options options)
{
   import core.thread : Thread;
   import core.time : seconds;

   import std.array : byPair;
   import std.stdio : stdout, stderr;
   import std.random : Random, uniform, unpredictableSeed;
   import app.util : makeSafe;

   string[][string] duplicatedAccounts = findDuplicateDirectories(config.outputDirectory);
   foreach(PairType pair; duplicatedAccounts.byPair) {
      string newName = makeSafe(pair[1][0]);
      try {
         newName = checkAndGetChoice(pair, config, options);
      } catch (PixivJSONException pje) {
         errorf("checkAndRemove for ID %s: %s", pair[0], pje.msg);
         if (options.interactive) {
            stderr.writefln("WARNING: Account ID %s is not valid", pair[0]);
            stderr.writefln("         %s", pje.msg);
            stderr.writefln("All names for ID %s", pair[0]);
            newName = getChoice(newName, pair[1]);
         } else {
            stderr.writefln("ERROR: Failed to compact account for ID %s", pair[0]);
            stderr.writefln("       %s", pje.msg);
            newName = null;
         }
      }

      if (newName !is null) {
         compact(newName, pair, config, options);
      }

      scope rnd = Random(unpredictableSeed);
      auto sleepDuration = uniform(3, 10, rnd);
      stderr.writefln("Sleeping for %d seconds...", sleepDuration);
      Thread.sleep(sleepDuration.seconds);
   }

   if (options.dryRun)
   {
      import std.stdio : writeln;
      writeln("    No files or directories were moved.");
   }
}

auto findDuplicateDirectories(string outputDirectory)
{
   import std.algorithm.iteration : each, filter;
   import std.algorithm.searching : countUntil;
   import std.array : byPair;
   import std.file : SpanMode, dirEntries;
   import std.path : baseName;

   // ["uid": ["name1", "name2", ...], "uid2": ...]
   string[][string] uids;

   dirEntries(outputDirectory, SpanMode.shallow).each!((dir) {
      const bname = baseName(dir);
      const splitIndex = countUntil!"a < '0' || a > '9'"(bname);
      if (splitIndex > 0) {
         const uid = bname[0..splitIndex];
         uids[uid] ~= bname[splitIndex+1..$];
      }
   });

   // D 2.076 compat: filter doesn't seem to like this predicate.
   //   return uids.byPair.filter!(pair => pair.value.length > 1)
   foreach(uid, names; uids) {
      if (names.length <= 1) {
         uids.remove(uid);
      }
   }
   return uids;
}

string checkAndGetChoice(PairType pair, const ref Config config, const ref Options options)
{
   import std.stdio : writefln;
   import app.util : makeSafe;

   // Default to the current name
   const user = fetchUser(pair[0], config);
   string newName = makeSafe(user.userName);

   if (options.interactive) {
      writefln("All names for ID %s", user.id);
      newName = getChoice(newName, pair[1]);
   }

   infof("newName = %s", newName);
   return newName;
}

void compact(string newName, PairType pair, const ref Config config, const ref Options options)
{
   import std.algorithm.iteration : each;
   import std.file : SpanMode, dirEntries, exists, mkdirRecurse;
   import std.path : buildPath;
   import std.stdio : stdout;

   const newDirName = buildPath(config.outputDirectory, pair[0] ~ "_" ~ newName);
   if (options.dryRun)
   {
      if (false == exists(newDirName))
      {
         stdout.writefln("Would create directory: %s", newDirName);
      }
   }
   else
   {
      mkdirRecurse(newDirName);
   }

   // copy old files to new directory
   foreach(oldDirUname; pair[1]) {
      const oldDirName = buildPath(config.outputDirectory, pair[0] ~ "_" ~ oldDirUname);

      if (oldDirName == newDirName) {
         continue;
      }

      dirEntries(oldDirName, SpanMode.shallow).each!((ent) {
         if (options.dryRun) {
            fakeMove(ent.name, newDirName);
         } else {
            move(ent.name, newDirName);
         }
      });

      if (false == options.dryRun) {
         import mlib.trash : trash;

         trash(oldDirName);
         infof("trashed %s", oldDirName);

         stdout.writefln("Moved all files and directories:\n\tFrom: %s\n\tTo:   %s", oldDirName,
            newDirName);
      } else {
         import std.range : repeat;
         import mlib.term;

         stdout.writefln("    %s", '-'.repeat(Term.getColumnCount() - 4));
      }
   }
}

/// Prompt to choose which directory name to use.
///
/// Params:
///   safeWord = should be the result of makeSafe on the current
///              pixiv account name.
///   validNames = the array of existing names used for directories.
///
/// Returns: The chosen name.
string getChoice(string safeWord, string[] validNames)
{
   import std.algorithm.searching : countUntil;
   import std.conv : to;
   import std.stdio : readln, writef;
   import std.string : strip;

   bool appendedSafeWord = false;

   // Default choice should be 'safeWord'
   const indexOfSafeWord = countUntil(validNames, safeWord);
   long choice = (indexOfSafeWord == -1) ? validNames.length + 1 : indexOfSafeWord;

   foreach(i, name; validNames) {
      writef("%2d: %s\n", i + 1, name);
   }

   if (indexOfSafeWord == -1) {
      writef("%2d: %s\n", validNames.length +1, safeWord);
      appendedSafeWord = true;
   }

lPromptForChoice:
   writef("Enter the number to use: ");
   const answer = readln.strip;
   if (answer == "") {
      return safeWord;
   }

   try {
      choice = to!long(answer);
   } catch (Exception) {
      goto lPromptForChoice;
   }

   const maxChoice = appendedSafeWord ? validNames.length + 1 : validNames.length;

   if (choice <= 0 || choice > maxChoice) {
      goto lPromptForChoice;
   }

   if (appendedSafeWord && choice == maxChoice) {
      return safeWord;
   }

   return validNames[choice - 1];
}

///
/// move *from* (absolute path) to *to* (absolute path).
///
/// *to* should not contain the new filename/dirname, but
/// be the absolute path to the new parent directory.
void move(string from, string to)
{
   import std.file : exists, rename;
   import std.path : baseName, buildPath;

   const newFilename = buildPath(to, baseName(from));

   if (false == exists(newFilename))
   {
      // TODO: Access to logging functions
      rename(from, newFilename);
   }
}

void fakeMove(string from, string to)
{
   import std.path : baseName, buildPath;
   import std.stdio : writefln;

   const newFilename = buildPath(to, baseName(from));

   writefln("    FROM: %s", from);
   writefln("      TO: %s", newFilename);
}
